<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

    <title>Blur Post-processing — Alien.js</title>

    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
    <link rel="stylesheet" href="../assets/css/style.css">

    <script type="module">
        import { BlurMaterial, Color, ColorManagement, CopyMaterial, DirectionalLight, Group, HemisphereLight, LinearSRGBColorSpace, Mesh, MeshStandardMaterial, OrthographicCamera, PanelItem, PerspectiveCamera, Scene, SphereGeometry, UI, Vector2, WebGLRenderTarget, WebGLRenderer, getFullscreenTriangle, ticker } from '../../build/alien.three.js';

        import periodic3d from '../../src/shaders/modules/noise/periodic3d.glsl.js';

        class SceneView extends Group {
            constructor() {
                super();

                this.initMesh();
            }

            initMesh() {
                const { time } = WorldController;

                const geometry = new SphereGeometry(1, 80, 80);

                const material = new MeshStandardMaterial({ roughness: 0 });

                // Based on https://github.com/spite/perlin-experiments

                material.onBeforeCompile = shader => {
                    shader.uniforms.time = time;

                    shader.vertexShader = shader.vertexShader.replace(
                        'void main() {',
                        /* glsl */ `
                        uniform float time;

                        ${periodic3d}

                        void main() {
                        `
                    );

                    shader.vertexShader = shader.vertexShader.replace(
                        '#include <begin_vertex>',
                        /* glsl */ `
                        float f = 0.05 * pnoise(vec3(2.0 * normal + time), vec3(10.0));
                        vec3 transformed = position + f * normal;
                        `
                    );
                };

                const mesh = new Mesh(geometry, material);
                this.add(mesh);
            }
        }

        class PanelController {
            static init(ui) {
                this.ui = ui;

                this.initPanel();
            }

            static initPanel() {
                const { hBlurMaterial, vBlurMaterial } = RenderManager;

                const items = [
                    {
                        name: 'FPS'
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'slider',
                        name: 'Blur',
                        min: 0,
                        max: 10,
                        step: 0.1,
                        value: hBlurMaterial.uniforms.uBlurAmount.value,
                        callback: value => {
                            hBlurMaterial.uniforms.uBlurAmount.value = value;
                            vBlurMaterial.uniforms.uBlurAmount.value = value;
                        }
                    }
                ];

                items.forEach(data => {
                    this.ui.addPanel(new PanelItem(data));
                });
            }
        }

        const BlurDirectionX = new Vector2(1, 0);
        const BlurDirectionY = new Vector2(0, 1);

        class RenderManager {
            static init(renderer, scene, camera) {
                this.renderer = renderer;
                this.scene = scene;
                this.camera = camera;

                this.blurResolutionScale = 0.25;
                this.blurAmount = 1.8;
                this.enabled = true;

                this.initRenderer();
            }

            static initRenderer() {
                const { screenTriangle } = WorldController;

                // Fullscreen triangle
                this.screenCamera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
                this.screen = new Mesh(screenTriangle);
                this.screen.frustumCulled = false;

                // Render targets
                this.renderTarget = new WebGLRenderTarget(1, 1, {
                    depthBuffer: false
                });

                this.renderTargetBlurA = this.renderTarget.clone();
                this.renderTargetBlurB = this.renderTarget.clone();

                this.renderTarget.depthBuffer = true;

                // Gaussian blur materials
                this.hBlurMaterial = new BlurMaterial(BlurDirectionX);
                this.hBlurMaterial.uniforms.uBlurAmount.value = this.blurAmount;

                this.vBlurMaterial = new BlurMaterial(BlurDirectionY);
                this.vBlurMaterial.uniforms.uBlurAmount.value = this.blurAmount;

                // Copy material
                this.copyMaterial = new CopyMaterial();
            }

            // Public methods

            static resize = (width, height, dpr) => {
                this.renderer.setPixelRatio(dpr);
                this.renderer.setSize(width, height);

                width = Math.round(width * dpr);
                height = Math.round(height * dpr);

                this.renderTarget.setSize(width, height);

                width = Math.round(width * this.blurResolutionScale);
                height = Math.round(height * this.blurResolutionScale);

                this.renderTargetBlurA.setSize(width, height);
                this.renderTargetBlurB.setSize(width, height);

                this.hBlurMaterial.uniforms.uResolution.value.set(width, height);
                this.vBlurMaterial.uniforms.uResolution.value.set(width, height);
            };

            static update = () => {
                const renderer = this.renderer;
                const scene = this.scene;
                const camera = this.camera;

                if (!this.enabled) {
                    renderer.setRenderTarget(null);
                    renderer.render(scene, camera);
                    return;
                }

                const renderTarget = this.renderTarget;
                const renderTargetBlurA = this.renderTargetBlurA;
                const renderTargetBlurB = this.renderTargetBlurB;

                // Scene pass
                renderer.setRenderTarget(renderTarget);
                renderer.render(scene, camera);

                // Two pass Gaussian blur (horizontal and vertical)
                this.hBlurMaterial.uniforms.tMap.value = renderTarget.texture;
                this.screen.material = this.hBlurMaterial;
                renderer.setRenderTarget(renderTargetBlurA);
                renderer.render(this.screen, this.screenCamera);

                this.vBlurMaterial.uniforms.tMap.value = renderTargetBlurA.texture;
                this.screen.material = this.vBlurMaterial;
                renderer.setRenderTarget(renderTargetBlurB);
                renderer.render(this.screen, this.screenCamera);

                // Copy pass (render to screen)
                this.copyMaterial.uniforms.tMap.value = renderTargetBlurB.texture;
                this.screen.material = this.copyMaterial;
                renderer.setRenderTarget(null);
                renderer.render(this.screen, this.screenCamera);
            };
        }

        class WorldController {
            static init() {
                this.initWorld();
                this.initLights();

                this.addListeners();
            }

            static initWorld() {
                this.renderer = new WebGLRenderer({
                    powerPreference: 'high-performance',
                    antialias: true
                });

                // Output canvas
                this.element = this.renderer.domElement;

                // Disable color management
                ColorManagement.enabled = false;
                this.renderer.outputColorSpace = LinearSRGBColorSpace;

                // 3D scene
                this.scene = new Scene();
                this.scene.background = new Color(0x060606);
                this.camera = new PerspectiveCamera(30);
                this.camera.near = 0.5;
                this.camera.far = 40;
                this.camera.position.z = 8;
                this.camera.lookAt(this.scene.position);

                // Global geometries
                this.screenTriangle = getFullscreenTriangle();

                // Global uniforms
                this.resolution = { value: new Vector2() };
                this.texelSize = { value: new Vector2() };
                this.aspect = { value: 1 };
                this.time = { value: 0 };
                this.frame = { value: 0 };
            }

            static initLights() {
                this.scene.add(new HemisphereLight(0x606060, 0x404040, 3));

                const light = new DirectionalLight(0xffffff, 2);
                light.position.set(1, 1, 1);
                this.scene.add(light);
            }

            static addListeners() {
                this.renderer.domElement.addEventListener('touchstart', this.onTouchStart);
            }

            // Event handlers

            static onTouchStart = e => {
                e.preventDefault();
            };

            // Public methods

            static resize = (width, height, dpr) => {
                this.camera.aspect = width / height;
                this.camera.updateProjectionMatrix();

                if (width < height) {
                    this.camera.position.z = 10;
                } else {
                    this.camera.position.z = 8;
                }

                width = Math.round(width * dpr);
                height = Math.round(height * dpr);

                this.resolution.value.set(width, height);
                this.texelSize.value.set(1 / width, 1 / height);
                this.aspect.value = width / height;
            };

            static update = (time, delta, frame) => {
                this.time.value = time;
                this.frame.value = frame;
            };
        }

        class App {
            static async init() {
                this.initWorld();
                this.initViews();
                this.initControllers();

                this.addListeners();
                this.onResize();

                this.initPanel();
            }

            static initWorld() {
                WorldController.init();
                document.body.appendChild(WorldController.element);
            }

            static initViews() {
                this.view = new SceneView();
                WorldController.scene.add(this.view);

                this.ui = new UI({ fps: true });
                this.ui.animateIn();
                document.body.appendChild(this.ui.element);
            }

            static initControllers() {
                const { renderer, scene, camera } = WorldController;

                RenderManager.init(renderer, scene, camera);
            }

            static initPanel() {
                PanelController.init(this.ui);
            }

            static addListeners() {
                window.addEventListener('resize', this.onResize);
                ticker.add(this.onUpdate);
                ticker.start();
            }

            // Event handlers

            static onResize = () => {
                const width = document.documentElement.clientWidth;
                const height = document.documentElement.clientHeight;
                const dpr = window.devicePixelRatio;

                WorldController.resize(width, height, dpr);
                RenderManager.resize(width, height, dpr);
            };

            static onUpdate = (time, delta, frame) => {
                WorldController.update(time, delta, frame);
                RenderManager.update(time, delta, frame);
                this.ui.update();
            };
        }

        App.init();
    </script>
</head>
<body>
</body>
</html>
